﻿using System;
using System.IO;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using EnvDTE;
using EnvDTE80;
using System.Windows;

namespace VsTortoiseSVN
{
    using SharpSvn;

    public struct SvnHelperStatusBools
    {
        public bool Zero { get; set; }
        public bool None { get; set; }
        public bool NotVersioned { get; set; }
        public bool Normal { get; set; }
        public bool Added { get; set; }
        public bool Missing { get; set; }
        public bool Deleted { get; set; }
        public bool Replaced { get; set; }
        public bool Modified { get; set; }
        public bool Merged { get; set; }
        public bool Conflicted { get; set; }
        public bool Ignored { get; set; }
        public bool Obstructed { get; set; }
        public bool External { get; set; }
        public bool Incomplete { get; set; }

        /// <summary>
        /// Creates an empty StatusBools object with all
        /// members set to false.
        /// </summary>
        public static SvnHelperStatusBools CreateEmpty()
        {
            SvnHelperStatusBools item = new SvnHelperStatusBools();
            return item;
        }

        /// <summary>
        /// Creates a StatusBools object where all members
        /// are set to true, that correspond to being under
        /// version control.
        /// </summary>
        public static SvnHelperStatusBools CreateVersioned()
        {
            SvnHelperStatusBools item = CreateEmpty();

            item.Normal = true;
            item.Added = true;
            item.Missing = true;
            item.Deleted = true;
            item.Replaced = true;
            item.Modified = true;
            item.Merged = true;
            item.Conflicted = true;
            item.Ignored = true;
            item.Obstructed = true;
            item.External = true;
            item.Incomplete = true;

            return item;
        }

        public static SvnHelperStatusBools CreateConflicted()
        {
            SvnHelperStatusBools item = CreateEmpty();
            item.Conflicted = true;
            return item;
        }

        public bool this[SvnStatus status]
        {
            get
            {
                switch (status)
                {
                    case SvnStatus.Zero: return Zero;
                    case SvnStatus.None: return None;
                    case SvnStatus.NotVersioned: return NotVersioned;
                    case SvnStatus.Normal: return Normal;
                    case SvnStatus.Added: return Added;
                    case SvnStatus.Missing: return Missing;
                    case SvnStatus.Deleted: return Deleted;
                    case SvnStatus.Replaced: return Replaced;
                    case SvnStatus.Modified: return Modified;
                    case SvnStatus.Merged: return Merged;
                    case SvnStatus.Conflicted: return Conflicted;
                    case SvnStatus.Ignored: return Ignored;
                    case SvnStatus.Obstructed: return Obstructed;
                    case SvnStatus.External: return External;
                    case SvnStatus.Incomplete: return Incomplete;
                    default: break;
                }

                return false;
            }

            set
            {
                switch (status)
                {
                    case SvnStatus.Zero: Zero = value; break;
                    case SvnStatus.None: None = value; break;
                    case SvnStatus.NotVersioned: NotVersioned = value; break;
                    case SvnStatus.Normal: Normal = value; break;
                    case SvnStatus.Added: Added = value; break;
                    case SvnStatus.Missing: Missing = value; break;
                    case SvnStatus.Deleted: Deleted = value; break;
                    case SvnStatus.Replaced: Replaced = value; break;
                    case SvnStatus.Modified: Modified = value; break;
                    case SvnStatus.Merged: Merged = value; break;
                    case SvnStatus.Conflicted: Conflicted = value; break;
                    case SvnStatus.Ignored: Ignored = value; break;
                    case SvnStatus.Obstructed: Obstructed = value; break;
                    case SvnStatus.External: External = value; break;
                    case SvnStatus.Incomplete: Incomplete = value; break;
                    default: break;
                }
            }
        }

        public void EnableAll()
        {
            Zero = true;
            None = true;
            NotVersioned = true;
            Normal = true;
            Added = true;
            Missing = true;
            Deleted = true;
            Replaced = true;
            Modified = true;
            Merged = true;
            Conflicted = true;
            Ignored = true;
            Obstructed = true;
            External = true;
            Incomplete = true;
        }
    }

    public class SvnHelperStatusItem
    {
        public System.IO.FileInfo Info { get; internal set; }
        public SvnStatusEventArgs Status { get; internal set; }
    }

    public static class SvnHelper
    {
        private class StatusCacheItem
        {
            public double CachedMilliseconds;
            public string Path;
            public SvnStatusEventArgs Status;

            public bool IsValid()
            {
                if (CachedMilliseconds + 1000 > DateTime.Now.TimeOfDay.TotalMilliseconds)
                    return true;

                return false;
            }
        }

        private static Dictionary<string, StatusCacheItem> StatusCache = new Dictionary<string, StatusCacheItem>(64);
        private static object thisLock = new object();

        public static SvnClient Client { get; private set; }

        static SvnHelper()
        {
            Client = new SvnClient();
        }

        private static string RepairPathCasing(string path)
        {
            // SVN is case sensitive, Visual Studio and the Windows FileSystem
            // use different casing for some reason, it's a mess. Callbacks provide
            // lower-case filenames sometimes, other functions correct casing again.
            // The following lines are more of a hack to fix incorrect casing of file pathes.
            path = PathHelper.GetShortPathName(path);
            path = PathHelper.GetLongPathName(path);
            return path;
        }

        public static bool Status(ICollection<SvnHelperStatusItem> dest, string path, SvnHelperStatusBools statuses)
        {
            lock (thisLock)
            {
                Guid guid;
                try
                {
                    path = RepairPathCasing(path);

                    // Very first step is to check if the specified folder
                    // is under source control at all.
                    if (Client.TryGetRepositoryId(path, out guid))
                    {
                        SvnStatusArgs statusArgs = new SvnStatusArgs();
                        statusArgs.RetrieveAllEntries = true;

                        // Get status of all items in the specified folder
                        // and sub-folders.
                        Client.Status(path, statusArgs,
                            delegate(object obj, SvnStatusEventArgs args)
                            {
                                if (statuses[args.LocalContentStatus])
                                {
                                    args.Detach();

                                    SvnHelperStatusItem item = new SvnHelperStatusItem();
                                    item.Info = new System.IO.FileInfo(args.FullPath);
                                    item.Status = args;
                                    dest.Add(item);
                                }
                            });

                        // Operation was successful
                        return true;
                    }

                }
                catch (SharpSvn.SvnInvalidNodeKindException /*e*/)
                {
                    //System.Windows.Forms.MessageBox.Show(e.ToString());
                }

                // Either not under version control or an
                // exception/error occurred.
                return false;
            }
        }

        public static bool HasLocalTextStatus(string path, SvnHelperStatusBools statuses)
        {
            SvnStatus status = LocalTextStatus(path);
            return statuses[status];
        }

        /// <summary>
        /// Gets whether the file with the specified path can be diffed.
        /// </summary>
        public static bool IsDiffable(string path)
        {
            SvnHelperStatusBools statuses = SvnHelperStatusBools.CreateVersioned();
            statuses.Ignored = false;
            statuses.Normal = false;

            bool result = SvnHelper.HasLocalTextStatus(path, statuses);
            return result;
        }

        public static bool IsVersionControlled(string path)
        {
            SvnHelperStatusBools statuses = SvnHelperStatusBools.CreateVersioned();
            statuses.Ignored = false;

            bool result = SvnHelper.HasLocalTextStatus(path, statuses);
            return result;
        }

        public static bool HasConflict(string path)
        {
            SvnHelperStatusBools statuses = SvnHelperStatusBools.CreateConflicted();

            bool result = SvnHelper.HasLocalTextStatus(path, statuses);
            return result;
        }

        public static bool UnifiedLocalDiff(string path, out string dest)
        {
            try
            {
                path = RepairPathCasing(path);

                SharpSvn.SvnDiffArgs diffArgs = new SharpSvn.SvnDiffArgs();
                diffArgs.RelativeToPath = Path.GetDirectoryName(path);
                diffArgs.SvnError += new EventHandler<SvnErrorEventArgs>(diffArgs_SvnError);

                MemoryStream diffResult = new MemoryStream();

                if (SvnHelper.Client.Diff(
                    new SharpSvn.SvnPathTarget(path, SharpSvn.SvnRevision.Base),
                    new SharpSvn.SvnPathTarget(path, SharpSvn.SvnRevision.Working),
                    diffArgs, diffResult))
                {
                    Byte[] content = new Byte[diffResult.Length];

                    diffResult.Position = 0;
                    diffResult.Read(content, 0, (int)diffResult.Length);

                    dest = Encoding.ASCII.GetString(content);
                    dest = dest.Replace('\0', ' ');
                    return true;
                }
            }
            catch //(Exception e)
            {
                //System.Windows.Forms.MessageBox.Show(e.Message);
            }

            dest = string.Empty;
            return false;
        }

        static void diffArgs_SvnError(object sender, SvnErrorEventArgs e)
        {
            Output.Instance.WriteLine(e.ToString() + ":");
            Output.Instance.WriteLine(e.Exception.Message);
        }

        public static bool FileStatus(string path, out SvnStatusEventArgs outStatus)
        {
            lock (thisLock)
            {
                path = RepairPathCasing(path);

                // Check if the status is cached already
                if (StatusCache.ContainsKey(path))
                {
                    StatusCacheItem cacheItem = StatusCache[path];

                    // Check if the cached status is within the lifetime threshols
                    if (cacheItem.IsValid())
                    {
                        outStatus = cacheItem.Status;
                        return outStatus != null;
                    }
                    else
                    {
                        // Cached item is too old, remove it and create a new one
                        StatusCache.Remove(path);
                    }
                }

                SvnStatusEventArgs tempArgs = null;
                Guid guid;
                string directory = Path.GetDirectoryName(path);
                if (Client.TryGetRepositoryId(directory, out guid))
                {
                    try
                    {
                        SvnStatusArgs statusArgs = new SvnStatusArgs();
                        statusArgs.RetrieveAllEntries = true;

                        Client.Status(path, statusArgs,
                            delegate(object obj, SvnStatusEventArgs args)
                            {
                                args.Detach();
                                tempArgs = args;
                            });
                    }
                    catch (SharpSvn.SvnInvalidNodeKindException)
                    {
                        // Directory seems not to be under Source Control
                    }
                }

                if(tempArgs != null)
                {
                    // Create a new cache item
                    StatusCacheItem cacheItem = new StatusCacheItem();
                    cacheItem.CachedMilliseconds = DateTime.Now.TimeOfDay.TotalMilliseconds;
                    cacheItem.Path = path;
                    cacheItem.Status = tempArgs;
                    StatusCache.Add(path, cacheItem);

                    outStatus = tempArgs;
                    return true;
                }

                outStatus = null;
                return false;
            }
        }

        public static SvnStatus LocalTextStatus(string path)
        {
            SvnStatusEventArgs status;
            if(FileStatus(path, out status))
                return status.LocalContentStatus;

            return SvnStatus.None;
        }

        public static bool CanAdd(string path)
        {
            SvnStatus stats = LocalTextStatus(path);
            return stats == SvnStatus.NotVersioned;
        }

        public static bool CanRevert(string path)
        {
            switch (LocalTextStatus(path))
            {
                case SvnStatus.Added: return true;
                case SvnStatus.Deleted: return true;
                case SvnStatus.Modified: return true;
                case SvnStatus.Replaced: return true;
                case SvnStatus.Conflicted: return true;
                default:
                    break;
            }

            return false;
        }

        public static bool IsLockedLocally(string path)
        {
            SvnStatusEventArgs status;
            if(FileStatus(path, out status))
            {
                return !string.IsNullOrEmpty(status.WorkingCopyInfo.LockToken);
            }

            return false;
        }
    }
}
